﻿
namespace DuplicateFileExplorer
{
	#region Using Directives
	using System;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.IO;
	using System.Linq;
	#endregion

	#region FileSearcher
	/// <summary>
	/// Searches for files asynchronously.
	/// </summary>
	public class FileSearcher
	{
		#region Events

		#region Errored
		/// <summary>
		/// Event raised, when the search has encountered an error
		/// and optional allows the user to cancel the current search.
		/// </summary>
		public event EventHandler<ErroredEventArgs> Errored;
		void OnErrored(Exception ex, ErroredEventArgs args)
		{
			var handler = this.Errored;
			if (null != handler)
				handler(this, args);
		}
		#endregion

		#region Canceled
		/// <summary>
		/// Event raised, when the search has been canceled.
		/// </summary>
		public event EventHandler<EventArgs> Canceled;
		void OnCanceled()
		{
			var handler = this.Canceled;
			if (null != handler)
				handler(this, EventArgs.Empty);
		}
		#endregion

		#region Completed
		/// <summary>
		/// Event raised, when the search has completed.
		/// </summary>
		public event EventHandler<EventArgs> Completed;
		void OnCompleted()
		{
			var handler = this.Completed;
			if (null != handler)
				handler(this, EventArgs.Empty);
		}
		#endregion

		#endregion

		#region Fields
		BackgroundWorker worker;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructor
		/// </summary>
		public FileSearcher()
		{
			this.SearchPaths = new List<string>();
			this.SearchPatterns = new List<string>();
			this.SearchResults = new List<FileSearchResultItem>();
			this.FailedDirectories = new List<DirectoryInfo>();
			this.ExcludeDirectories = new List<string>();
			this.ExcludeExtensions = new List<string>();
			this.IncludesSubdirectories = false;
			this.CreatedAfter = DateTime.MinValue;
			this.CreatedBefore = DateTime.MaxValue;
			this.ModifiedAfter = DateTime.MinValue;
			this.ModifiedBefore = DateTime.MaxValue;
		}
		#endregion

		#region Properties
		public List<string> SearchPaths { get; set; }
		public List<string> SearchPatterns { get; set; }
		public List<string> ExcludeDirectories { get; set; }
		public List<string> ExcludeExtensions { get; set; }
		public List<FileSearchResultItem> SearchResults { get; private set; }
		public List<DirectoryInfo> FailedDirectories { get; set; }
		public Exception Error { get; private set; }
		public DateTime ModifiedAfter { get; set; }
		public DateTime ModifiedBefore { get; set; }
		public DateTime CreatedAfter { get; set; }
		public DateTime CreatedBefore { get; set; }
		public bool HasError { get { return (null != this.Error); } }
		public bool IsBusy
		{
			get
			{
				if (null != this.worker)
					return this.worker.IsBusy;
				return false;
			}
		}
		public bool IncludesSubdirectories { get; set; }
		#endregion

		#region Methods

		/// <summary>
		/// Begins an asynchronous search.
		/// </summary>
		/// <param name="paths"></param>
		/// <param name="searchPattern"></param>
		/// <param name="includeSubdirectories"></param>
		public void SearchAsync(List<string> paths, List<string> searchPatterns, bool includeSubdirectories)
		{
			this.SearchAsync(paths, searchPatterns, includeSubdirectories, null, null, DateTime.MinValue, DateTime.MaxValue, DateTime.MinValue, DateTime.MaxValue);
		}
		/// <summary>
		/// Begins an asynchronous search.
		/// </summary>
		/// <param name="paths"></param>
		/// <param name="searchPatterns"></param>
		/// <param name="includeSubdirectories"></param>
		/// <param name="excludeDirectories"></param>
		/// <param name="excludeExtensions"></param>
		/// <param name="createdAfter"></param>
		/// <param name="createBefore"></param>
		/// <param name="modifiedAfter"></param>
		/// <param name="modifiedBefore"></param>
		public void SearchAsync(List<string> paths, List<string> searchPatterns, bool includeSubdirectories, List<string> excludeDirectories, List<string> excludeExtensions, DateTime createdAfter, DateTime createBefore, DateTime modifiedAfter, DateTime modifiedBefore)
		{
			if (null != worker && worker.IsBusy)
				throw new InvalidOperationException("Cannot begin a new search until the current search has completed or cancelled.");

			if (null != this.SearchResults)
				this.SearchResults.Clear();
			this.SearchResults = new List<FileSearchResultItem>();
			if (null != this.FailedDirectories)
				this.FailedDirectories.Clear();
			this.FailedDirectories = new List<DirectoryInfo>();
			this.ExcludeDirectories = excludeDirectories;
			if (null == this.ExcludeDirectories)
				this.ExcludeDirectories = new List<string>();
			this.ExcludeExtensions = excludeExtensions;
			if (null == this.ExcludeExtensions)
				this.ExcludeExtensions = new List<string>();
			this.SearchPaths = paths;
			this.SearchPatterns = searchPatterns;
			this.IncludesSubdirectories = includeSubdirectories;
			this.CreatedAfter = createdAfter;
			this.CreatedBefore = createBefore;
			this.ModifiedAfter = modifiedAfter;
			this.ModifiedBefore = modifiedBefore;
			this.Error = null;

			worker = new BackgroundWorker();
			worker.DoWork += new DoWorkEventHandler(worker_DoWork);
			worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
			worker.WorkerSupportsCancellation = true;
			worker.RunWorkerAsync();
		}
		/// <summary>
		/// Attempts to cancel the search.
		/// </summary>
		public void CancelAsync()
		{
			worker.CancelAsync();
		}

		void Search(DoWorkEventArgs e)
		{
			foreach (var path in this.SearchPaths)
			{
				if (this.IsCancelled(e))
					break;
				if ((path.Length >= 3) && (Directory.Exists(path)))
				{
					if (this.SearchPatterns.Count > 0)
					{
						var stack = new Stack<DirectoryInfo>();
						stack.Push(new DirectoryInfo(path));

						while (stack.Count > 0)
						{
							if (this.IsCancelled(e))
								break;
							var dir = stack.Pop();
							if (!this.SearchDirectory(dir, e))
								continue;
							if (this.IsCancelled(e))
								break;
							if (this.IncludesSubdirectories)
							{
								foreach (var directory in dir.GetDirectories())
								{
									if (this.ExcludeDirectories.Contains(directory.Name, new StringComparer()))
										continue;
									stack.Push(directory);
								}
							}
						}
					}
				}
				else
				{
					throw new DirectoryNotFoundException("The directory:" + Environment.NewLine + path + Environment.NewLine + "does not exist.");
				}
			}
		}
		bool SearchDirectory(DirectoryInfo dirInfo, DoWorkEventArgs e)
		{
			bool result = true;
			foreach (var searchPattern in this.SearchPatterns)
			{
				if (this.IsCancelled(e))
					break;

				try
				{
					var files = dirInfo.GetFiles(searchPattern);
					foreach (var file in files)
					{
						if (this.IsCancelled(e))
							break;

						if (this.ExcludeExtensions.Contains(file.Extension, new StringComparer()))
							continue;

						if (this.CreatedAfter > DateTime.MinValue && file.CreationTime < this.CreatedAfter)
							continue;

						if (this.CreatedBefore < DateTime.MaxValue && file.CreationTime > this.CreatedBefore)
							continue;

						if (this.ModifiedAfter > DateTime.MinValue && file.LastWriteTime < this.ModifiedAfter)
							continue;

						if (this.ModifiedBefore < DateTime.MaxValue && file.LastWriteTime > this.ModifiedBefore)
							continue;

						this.SearchResults.Add(new FileSearchResultItem(file));
					}
				}
				catch (UnauthorizedAccessException)
				{
					this.FailedDirectories.Add(dirInfo);
					return false;
				}
				catch (Exception ex)
				{
					var args = new ErroredEventArgs();
					args.Error = ex;
					args.CurrentDirectory = dirInfo.FullName;
					args.CurrentSearchPattern = searchPattern;
					this.OnErrored(ex, args);
					if (args.Cancel)
						this.CancelAsync();
					return false;
				}
			}
			return result;
		}
		void SearchCompleted(RunWorkerCompletedEventArgs e)
		{
			this.worker.Dispose();
			this.worker = null;

			this.Error = e.Error;

			if (e.Cancelled)
			{
				this.OnCanceled();
				return;
			}

			this.OnCompleted();

		}
		bool IsCancelled(DoWorkEventArgs e)
		{
			if (this.worker.CancellationPending)
			{
				e.Cancel = true;
				return true;
			}
			return false;
		}

		#endregion

		#region Event Handlers
		void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
		{
			this.SearchCompleted(e);
		}
		void worker_DoWork(object sender, DoWorkEventArgs e)
		{
			this.Search(e);
		}
		#endregion

		#region StringComparer
		class StringComparer : IEqualityComparer<string>
		{
			public bool Equals(string x, string y)
			{
				return x.Equals(y, StringComparison.InvariantCultureIgnoreCase);
			}

			public int GetHashCode(string obj)
			{
				return obj.GetHashCode();
			}
		}
		#endregion
	}
	#endregion
}